iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
Modern Web

React TDD 實戰:用 Vitest 打造可靠的前端應用系列 第 10

Day 10 - 重構與測試:讓程式碼持續進化 🔧

  • 分享至 

  • xImage
  •  

還記得第一次接手別人寫的程式碼嗎?那種「這是什麼?」的困惑、「為什麼要這樣寫?」的疑問,以及「我該從哪裡開始改?」的無助感。每個開發者都有過這樣的經歷。

經過前九天的學習,我們掌握了 TDD 的基本工具。今天來學習「重構與測試」:在不改變程式外部行為的前提下,改善程式碼內部結構。這就像整理房間一樣,外觀看起來還是同一個房間,但內部變得井然有序、使用起來更方便。

有了測試作為安全網,重構就變得安全而有信心。測試會告訴你:「重構是否保持了原有的行為」。這就像走鋼索時下面有安全網,讓你可以大膽前進。

本日學習地圖 🗺️

重構之旅啟程!
├── 第一站:理解重構的本質
├── 第二站:測試驅動的重構實戰
├── 第三站:掌握重構技巧
├── 第四站:重構最佳實踐
└── 終點站:10天學習總結與回顧

學習目標 🎯

今天你將學會:

  • 理解重構的概念和重要性
  • 掌握常見的重構技巧
  • 學會在測試保護下進行安全重構
  • 總結前 10 天的 TDD 學習成果

什麼是重構?🔄

重構是透過小步驟改善程式碼結構,同時保持程式的外部行為不變。Martin Fowler 在《Refactoring》一書中說:「重構是在不改變軟體可觀察行為的前提下,改善其內部結構」。

重構 vs 重寫

很多人常把重構和重寫搞混:

特性 重構 重寫
改變外部行為 ❌ 否 ✅ 可能
需要測試保護 ✅ 必須 ⚠️ 不一定
風險程度
進行方式 小步驟 大範圍
時間投入 持續進行 一次性

為什麼要重構? 💡

  1. 提升可讀性:有意義的命名、清晰的結構
  2. 減少重複:遵循 DRY 原則,提取共用邏輯
  3. 提升維護性:降低修改成本,加快開發速度
  4. 降低複雜度:分解大函數,簡化條件判斷

何時該重構? ⏰

三法則(Rule of Three):

  1. 第一次做某件事時,直接做
  2. 第二次做類似的事時,會有點不情願但還是做了
  3. 第三次做類似的事時,就該重構了

測試驅動的重構 🚀

重構的黃金法則:在重構之前,你必須有穩固的測試。沒有測試的重構是危險的,就像沒有安全帶就開車一樣。

重構的安全步驟

1. 確認測試都是綠燈 ✅
2. 執行小步驟重構 🔧
3. 執行測試驗證 🧪
4. 如果測試失敗,立即回復 ↩️
5. 重複直到完成 🔄

讓我們透過實際案例來體驗重構的過程:

建立 src/day10/calculator.ts

export class Calculator {
  // 需要重構的複雜函式
  calculate(a: number, b: number, operation: string): number {
    if (operation === 'add') {
      return a + b
    } else if (operation === 'subtract') {
      return a - b
    } else if (operation === 'multiply') {
      return a * b
    } else if (operation === 'divide') {
      if (b === 0) {
        throw new Error('Cannot divide by zero')
      }
      return a / b
    } else {
      throw new Error('Unknown operation')
    }
  }
}

重構前的測試

建立 tests/day10/calculator.test.ts

import { describe, it, expect } from 'vitest'
import { Calculator } from '../../src/day10/calculator.js'

describe('Calculator Tests', () => {
  const calculator = new Calculator()

  it('performs basic arithmetic operations', () => {
    expect(calculator.calculate(5, 3, 'add')).toBe(8)
    expect(calculator.calculate(5, 3, 'subtract')).toBe(2)
    expect(calculator.calculate(5, 3, 'multiply')).toBe(15)
    expect(calculator.calculate(6, 2, 'divide')).toBe(3)
  })

  it('handles errors correctly', () => {
    expect(() => calculator.calculate(5, 0, 'divide'))
      .toThrow('Cannot divide by zero')
    expect(() => calculator.calculate(5, 3, 'unknown'))
      .toThrow('Unknown operation')
  })
})

執行重構

更新 src/day10/calculator.ts

export class Calculator {
  calculate(a: number, b: number, operation: string): number {
    const operations: Record<string, () => number> = {
      add: () => this.add(a, b),
      subtract: () => this.subtract(a, b),
      multiply: () => this.multiply(a, b),
      divide: () => this.divide(a, b)
    }

    const operationFn = operations[operation]
    if (!operationFn) {
      throw new Error('Unknown operation')
    }

    return operationFn()
  }

  private add(a: number, b: number): number {
    return a + b
  }

  private subtract(a: number, b: number): number {
    return a - b
  }

  private multiply(a: number, b: number): number {
    return a * b
  }

  private divide(a: number, b: number): number {
    if (b === 0) {
      throw new Error('Cannot divide by zero')
    }
    return a / b
  }
}

常用重構技巧 🎯

1. 提取方法(Extract Method)

// 重構前:所有邏輯混在一起
function processOrder(order: Order): void {
  if (!order.customer) throw new Error('Customer required')
  let total = 0
  for (const item of order.items) {
    total += item.price * item.quantity
  }
  if (order.customer.type === 'VIP') total *= 0.9
  order.total = total
}

// 重構後:清晰分離
function processOrder(order: Order): void {
  validateOrder(order)
  const total = calculateTotal(order)
  const discountedTotal = applyDiscount(total, order.customer)
  updateOrder(order, discountedTotal)
}

2. 提取變數(Extract Variable)

// 重構前:複雜表達式
function calculatePrice(base: number, type: string, amount: number): number {
  return base * (type === 'VIP' ? 0.8 : 1.0) * (amount > 1000 ? 0.95 : 1.0)
}

// 重構後:清晰的變數
function calculatePrice(base: number, type: string, amount: number): number {
  const customerDiscount = type === 'VIP' ? 0.8 : 1.0
  const volumeDiscount = amount > 1000 ? 0.95 : 1.0
  return base * customerDiscount * volumeDiscount
}

3. 消除重複(Remove Duplication)

建立 tests/day10/remove-duplication.test.ts

import { describe, it, expect } from 'vitest'

describe('Remove Duplication Refactoring', () => {
  // 重構後:提取共用驗證
  class UserService {
    private validateUserId(userId: string): void {
      if (!userId || userId.trim() === '') {
        throw new Error('Invalid user ID')
      }
    }

    updateEmail(userId: string, email: string): void {
      this.validateUserId(userId)
      if (!email || !email.includes('@')) {
        throw new Error('Invalid email')
      }
    }

    updatePassword(userId: string, password: string): void {
      this.validateUserId(userId)
      if (!password || password.length < 8) {
        throw new Error('Invalid password')
      }
    }
  }

  it('validates inputs consistently', () => {
    const service = new UserService()
    
    expect(() => service.updateEmail('', 'test@test.com'))
      .toThrow('Invalid user ID')
    expect(() => service.updateEmail('user123', 'invalid'))
      .toThrow('Invalid email')
  })
})

重構的最佳實踐 ✨

1. 小步驟重構

每次只改一點點 → 執行測試 → 提交版本控制

2. 測試驗證

$ npm test  # 確認綠燈 → 重構 → 再測試 → 提交

3. 向後相容

getTotal() {  // 保留舊介面
  console.warn('deprecated')
  return this.calculateTotal()  // 呼叫新實作
}

4. 重構時機

✅ 適合:新功能前、修 bug 時、Code Review 時
❌ 不適:截止期近、無測試保護、即將淘汰

第一階段總結:10 天 TDD 基礎之旅 🎓

恭喜你!完成了 TDD 第一階段的學習。讓我們回顧這 10 天的精彩旅程:

學習軌跡回顧 📈

Day 01-03:環境設定、斷言基礎、紅綠重構循環
Day 04-06:測試結構、生命週期、參數化測試
Day 07-09:測試替身、例外處理、覆蓋率分析
Day 10:重構與測試的完美搭配

關鍵收穫總結 💎

  1. 測試優先思維:從需求出發,測試即文件、測試即設計
  2. 小步驟開發:每次只改一點點,快速獲得回饋
  3. 重構信心:測試是安全網,持續改進品質
  4. 品質意識:測試促進良好的設計模式

你已經掌握的能力 ⚡

✅ Vitest 測試框架 | ✅ 斷言方法 | ✅ TDD 循環
✅ 測試組織 | ✅ 生命週期 | ✅ 參數化測試
✅ 測試替身 | ✅ 例外測試 | ✅ 覆蓋率分析 | ✅ 安全重構

第一階段基礎訓練圓滿完成!🎉

今天學到什麼?📝

核心概念

  1. 重構的本質:在保持外部行為不變下改善內部結構
  2. 測試安全網:讓重構變得安全而有信心
  3. 重構技巧:提取方法、提取變數、消除重複
  4. 重構流程:小步驟、持續測試、向後相容
  5. 10 天總結:建立完整 TDD 基礎

總結 🎊

今天我們學會了測試驅動的重構,有了測試作為安全網,我們可以大膽地持續改善程式碼。

重要心法

「寫程式」是為了讓機器理解
「重構」是為了讓人類理解  
「測試」是為了讓改變安全

第一階段學習圓滿結束!記住 TDD 精髓:紅 → 綠 → 重構。恭喜完成前十天的 TDD 基礎學習!🚀


上一篇
Day 09 - 測試覆蓋率:你的測試真的夠完整嗎? 📊
下一篇
Day 11 - Kata 介紹與設置 🎯
系列文
React TDD 實戰:用 Vitest 打造可靠的前端應用28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言